package com.course.redisson.service;

import com.course.model.dto.UserDto;
import com.course.model.entity.User;
import com.course.model.entity.UserExample;
import com.course.model.exception.BusinessException;
import com.course.model.exception.BusinessExceptionCode;
import com.course.model.mapper.UserMapper;
import com.course.model.util.CopyUtil;
import com.course.model.util.RedisUtil;
import com.course.model.util.UuidUtil;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

/**
 * @PROJECT_NAME: course
 * @DESCRIPTION:
 * @Author: 涂玄武
 * @DATE: 2021/6/4 11:01
 */
@Service
public class UserRegisterLock {

    private static final Logger logger = LoggerFactory.getLogger(UserRegisterLock.class);

    @Resource
    private UserMapper userMapper;

    @Resource
    private RedissonClient redissonClient;

    /**
     * 新增(处理用户提交注册的请求，加Redisson分布式锁)
     * 一次性锁应用场景：适用于在同一时刻，且在很长一段时间内仍然只允许只有一个线程访问共享资源的场景，如：用户注册、重复提交、抢红包
     * 可重入锁应用场景：适用于同一时刻并发产生多线程时，虽然在某一时刻不能获取到分布式搜，但是却允许隔一定的时间重新获取并操作共享资源的场景，如商城高并发抢购商品
     * 主要通过lock.lock()实现
     */
    private void userRegRedisson(UserDto userDto) {

        //原逻辑不加锁
//        user.setId(UuidUtil.getShortUuid());
//        Optional<User> optional = Optional.ofNullable(this.selectByLoginName(user.getLoginName()));
//        if (optional.isPresent()) {
//            throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
//        } else {
//            userMapper.insert(user);
//        }

        //定义锁的名称
        final String lockName = RedisUtil.keyBuilder("Redisson", "redissonOneLock", "userRegister", userDto.getLoginName());
        //获取分布式锁实例
        /**
         * 原理解析：当我们调用redisson.getLock()时，程序会初始化一个默认的同步执行器RedissonLock
         *         这里面初始化了几个参数：
         *              (1)commandExecutor:异步的Executor执行器，Redisson中所有的命令都是通过...Executor 执行的 ；
         *              (2)internalLockLeaseTime：等待获取锁时间，这里读的是配置类中默认定义的，时间为30秒；
         *              (3)getEntryName():返回锁名称，代表当前线程持有对应锁的一个标识
         * 文章链接：https://zhuanlan.zhihu.com/p/361440201,https://www.zhihu.com/people/xue-jia-tao-51/posts
         *         1.核心获取锁的方法-tryLockInnerAsync:
         *              1）第一个If判断key是否存在，不存在则完成加锁操作;
         *              2）第二个if判断key存在且当前线程已经持有锁, 重入;
         *              3)key存在被其他线程获取的锁, 等待;
         *         其中pexpire语句的重置锁超时时间，实际上就是Redisson的watch dog机制。
         *         关于超时时间的计算，使用的是config中的Timeout时间+重试周期x重试次数；
         *         当RFuture等待超时时，就会使用tryFailure抛出RedisTimeoutException的异常信息，提示订阅失败。
         *         2.锁等待 lockInterruptibly
         *              1)调用加锁操作；
         *              2)步骤一中加锁操作失败，订阅消息，利用redis的pubsub提供一个通知机制来减少不断的重试，避免发生活锁;
         *              3)getLath()获取RedissionLockEntry实例latch变量，由于permits为0，所以调用acquire()方法后线程阻塞。
         *         3.释放锁 - unlockInnerAsync
         *              1)判断锁是否存在，不存在的话用publish命令发布释放锁的消息，订阅者收到后就能做下一步的拿锁处理；
         *              2)锁存在但不是当前线程持有，返回空置nil；
         *              3)当前线程持有锁，用hincrby命令将锁的可重入次数-1，然后判断重入次数是否大于0，是的话就重新刷新锁的过期时长，返回0，否则就删除锁，并发布释放锁的消息，返回1；
         *              4)当线程完全释放锁后，就会调用cancelExpirationRenewal()方法取消"看门狗"的续时线程
         *
         *         redisson-watchdog使用和原理:
         *              首先watchdog的具体思路是:加锁时，默认加锁 30秒，每30/3秒钟检查一次，如果存在就重新设置 过期时间为30秒。
         *              然后设置默认加锁时间的参数是 lockWatchdogTimeout（监控锁的看门狗超时，单位：毫秒）
         *
         *         在调用lock方法时，会最终调用到tryAcquireAsync:
         *              //如果指定了加锁时间，会直接去加锁
         *              //没有指定加锁时间 会先进行加锁，并且默认时间就是 LockWatchdogTimeout的时间
         *              //开启一个监听器，如果发现拿到锁了，就开启定时任务不断去刷新该锁的过期时长
         *
         *          总结:
         *              1.要使 watchLog机制生效 ，lock时 不要设置 过期时间
         *
         *              2.watchlog的延时时间 可以由 lockWatchdogTimeout指定默认延时时间，但是不要设置太小。如100
         *
         *              3.watchdog 会每 lockWatchdogTimeout/3时间，去延时。
         *
         *              4.watchdog 通过 类似netty的 Future功能来实现异步延时
         *
         *              5.watchdog 最终还是通过 lua脚本来进行延时
         *
         */
        RLock lock = redissonClient.getLock(lockName);
        try {
            //操作共享资源之前上锁
            //上锁之后，不管何种状况，10s后会自动释放
            lock.lock(10L, TimeUnit.SECONDS);

            //TODO: 以下为真正的核心处理逻辑
            //根据登录用户名查询用户实体信息
            Optional<User> optional = Optional.ofNullable(this.selectByLoginName(userDto.getLoginName()));
            if (optional.isPresent()) {
                //如果用户名已被注册，则抛出异常
                throw new BusinessException(BusinessExceptionCode.USER_LOGIN_NAME_EXIST);
            } else {
                userDto.setId(UuidUtil.getShortUuid());
                //插入用户注册信息
                userMapper.insert(CopyUtil.copy(userDto,User.class));
            }
        } catch (BusinessException e) {
            logger.error("---获取Redisson的分布式锁失败！---");
            throw e;
        } finally {
            //TODO: 不管发生何种情况，在处理完核心业务逻辑之后，需要释放该分布式锁
            if (Objects.nonNull(lock)){
                //释放锁
                lock.unlock();
                //在某些严格的业务场景下，也可以调用强制释放分布式锁的方法
                lock.forceUnlock();
            }
        }
    }

    /**
     * 根据登录名查询用户记录
     * 在同一时刻有100个相同的注册信息同时涌入，插入时来不及反应造成多个相同的用户名注册成功（高并发问题）
     * @param loginName
     * @return
     */
    public User selectByLoginName(String loginName) {
        UserExample example = new UserExample();
        UserExample.Criteria criteria = example.createCriteria();
        criteria.andLoginNameEqualTo(loginName);
        List<User> users = userMapper.selectByExample(example);
        if (CollectionUtils.isEmpty(users)) {
            return null;
        } else {
            return users.get(0);
        }
    }
}
